package Simulator;

import java.util.LinkedList;
import java.util.List;

import Shared.AmbState;
import Shared.AmbType;
import Shared.MapPosition;
import Shared.MsgType;
import Shared.RadioMessage;
import Shared.Utils;
import Simulator.MdtGUI.MDTScreen;

/**
 * The MDT module, see Architecture report for specifications.
 */

public class MDT implements Runnable {
	public static final int UP = 0;
	public static final int DOWN = 1;
	public static final int LEFT = 2;
	public static final int RIGHT = 3;

	private Radio radio;
	private RealAmbulance amb;
	private GPS gps;
	private MapPosition assignedDestination;

	private List<LogEntry> log;
	private Thread radioThread;
	private boolean monitoring;

	private MDTScreen gui;

	/**
	 * Creates an MDT for ambulance "amb" with Radio "radio".
	 * 
	 * @param amb
	 *            the ambulance linked to the MDT
	 * @param radio
	 *            the radio linked to the MDT
	 */
	public MDT(RealAmbulance amb, Radio radio) {
		this.gui = null;
		this.amb = amb;
		this.gps = new GPS(amb);
		this.radio = radio;
		this.log = new LinkedList<LogEntry>();

		this.monitoring = false;
	}

	/**
	 * Pre : The ambulance is not yet registered.<br />
	 * 
	 * Sends a message to dispatching system to register this ambulance, sets
	 * the ambulance as registered and starts the GPS.
	 */
	public void register() {
		assert !this.amb.isRegistered() : "The ambulance is already registered.";

		this.monitoring = true;
		this.radioThread = new Thread(this);

		this.radio.send(MsgType.REGISTERING, this.amb.getID(), this.amb.getType());

		this.addLogEntry(new LogEntry("Registering ambulance.", LogEntry.SENT));

		this.amb.register();

		// start thread only afther register. Else assert error.
		this.radioThread.setName("mdt-" + amb.getID());
		this.radioThread.start();
	}

	/**
	 * Pre : The ambulance is registered.<br />
	 * 
	 * Sends a message to dispatching system to unregister this ambulance, sets
	 * the ambulance as unregistered.
	 */
	public void unregister() {
		assert this.amb.isRegistered() : "The ambulance isn't registered.";

		synchronized (this) {
			this.monitoring = false;
		}

		if (this.amb.isLogged()) {
			this.logout();
		}

		this.radio.send(MsgType.DEAD, this.amb.getID());
		this.addLogEntry(new LogEntry("This ambulance is removed from the system.", LogEntry.SENT));

		this.amb.unregister();

	}

	/**
	 * Pre : The ambulance has to be registered in the dispatching system.<br />
	 * 
	 * Log the crew into the dispatching system to announce that it is available
	 * for an assignment.
	 */
	public void login() {
		assert this.amb.isRegistered() : "The ambulance is not registered.";

		this.amb.logIn();
		this.radio.send(MsgType.LOGGING_IN, this.amb.getID());
		this.addLogEntry(new LogEntry("Logging ambulance.", LogEntry.SENT));

	}

	/**
	 * Pre : The ambulance has to be registered and logged in the dispatching.<br />
	 * system.<br />
	 * 
	 * Unlog the crew from the dispatching system.
	 */
	public void logout() {
		assert this.amb.isRegistered() : "The ambulance is not registered.";
		assert this.amb.isLogged() : "The ambulance is not logged in.";

		if (this.amb.isOnMission()) {
			this.radio.send(MsgType.MISSION_FAIL, this.amb.getID());
			this.addLogEntry(new LogEntry("Mission Failed.", LogEntry.SENT));
			this.assignedDestination = null;
		} else {
			this.radio.send(MsgType.LOGGING_OUT, this.amb.getID());
			this.addLogEntry(new LogEntry("Logging out.", LogEntry.SENT));
		}

		this.amb.logOut();

	}

	/**
	 * Pre : The ambulance has to be registered.<br />
	 * 
	 * Unlog the crew if it is logged. Updates the ambulance status as crashed.
	 */
	public void crash() {
		assert this.amb.isRegistered() : "The ambulance is not registered.";

		if (this.amb.isLogged()) {
			this.logout();
		}

		this.monitoring = false;

		this.radio.send(MsgType.DEAD, this.amb.getID());
		this.addLogEntry(new LogEntry("Crashed.", LogEntry.SENT));

		this.amb.crash();
	}

	/**
	 * Pre : The ambulance has to be crashed.<br />
	 * 
	 * Repairs the ambulance and sets it as Unregistered.
	 */
	public void gotRepaired() {
		assert amb.isInErrorState();

		this.addLogEntry(new LogEntry("Got repaired.", LogEntry.SENT));
		this.amb.clearError();
	}

	/**
	 * PRE : The ambulance has to be registered in the dispatching system.<br />
	 * 
	 * Listens to events from Radio and acts accordingly.
	 */
	public void monitorEvents() {

		RadioMessage objectif;
		try {
			while (this.monitoring) {
				synchronized (this) {
					if ((objectif = this.radio.listen(20)) != null) {

						if (objectif.getType() == MsgType.POSITION) {
							this.addLogEntry(new LogEntry("Received a new mission.", LogEntry.RECEIVED));
							this.assignDestination(objectif.getPosition());
							this.radio.send(MsgType.ON_MY_WAY, amb.getID());

						} else if (objectif.getType() == MsgType.AREYOUFREE) {
							if (amb.isFree()) {
								amb.assign();
								this.radio.send(MsgType.YES_FREE, amb.getID());
							} else
								this.radio.send(MsgType.SORRY_I_AM_BUSY, amb.getID());
						}
					}

					MapPosition current = this.gps.getAmbulancePosition();
					this.radio.sendAmbulancePosition(current, amb.getID());

					if (this.amb.isOnMission() && this.detectArrival()) {
						// TODO check that amb.isOnMission assures that we have
						// an assignedDestination
						Utils.disp("MDT>>> Arrival detected ...");
						// TODO Do we have to remove the ER now ?
						EnvironmentDatabase.getInstance().remER(amb.getMdt().getAmbulancePosition());
						this.finishMission();
					}
				}
				Thread.sleep(100);

			}

		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}

	}

	/**
	 * PRE : The ambulance has to be registered in the dispatching system.<br />
	 * 
	 * Start monitorEvents().
	 */
	public void run() {
		assert this.amb.isRegistered() : "The ambulance is not registered.";

		this.monitorEvents();
	}

	/**
	 * PRE : The ambulance has to be registered in the dispatching system.<br />
	 * 
	 * If the ambulance is available, acknowledge the mission to the dispatching
	 * system. Set the ambulance as assigned to this destination.
	 * 
	 * If it is unavailable, announce it to the dispatching system.
	 * 
	 * @param destination
	 *            The destination of the emergency it is assigned to.
	 */

	private void assignDestination(MapPosition destination) {
		assert this.amb.isRegistered() : "The ambulance is not registered.";

		this.addLogEntry(new LogEntry("Mission accepted", LogEntry.SENT));
		this.assignedDestination = destination;
		this.amb.acceptMission();
	}

	/**
	 * PRE : The ambulance has to be assigned to an Emergency.<br />
	 * 
	 * Detect if the ambulance is in the target area of the ER it has been
	 * assigned to.
	 * 
	 * @return true if the ambulance is at its assigned destination.
	 */
	private boolean detectArrival() {
		assert this.amb.isRegistered() : "The ambulance is not registered.";
		assert this.amb.isLogged() : "The ambulance is not logged in.";
		assert this.amb.isOnMission() : "The ambulance is not on a mission.";

		return this.gps.computePath(this.getAssignedDestination()) == null;
	}

	/**
	 * Pre : The ambulance has to be registered and logged in the dispatching
	 * system. The ambulance has to be on a mission.<br />
	 * 
	 * Set the mission as being finished and the ambulance as available. Send
	 * the ambulance's updated status through the radio.
	 */
	private void finishMission() {
		assert this.amb.isRegistered() : "The ambulance is not registered.";
		assert this.amb.isLogged() : "The ambulance is not logged in.";
		assert this.amb.isOnMission() : "The ambulance is not on a mission.";

		this.radio.send(MsgType.MISSION_SUCCESSFUL, this.amb.getID());
		this.addLogEntry(new LogEntry("Mission successful.", LogEntry.SENT));

		this.amb.setMissionFinished();
		this.assignedDestination = null;
	}

	/**
	 * 
	 * @return the MDT's ambulance position.
	 */
	public MapPosition getAmbulancePosition() {
		return this.gps.getAmbulancePosition();
	}

	/**
	 * @return the assigned destination, i.e. the emergency location if the
	 *         ambulance is assigned.
	 */
	public MapPosition getAssignedDestination() {
		return assignedDestination;
	}

	/**
	 * @return the status of the MDT's ambulance
	 */
	public AmbState getAmbulanceStatus() {
		return this.amb.getStatus();
	}

	/**
	 * 
	 * @return the ambulance type of the MDT's ambulance
	 */
	public AmbType getAmbulanceType() {
		return this.amb.getType();
	}

	/**
	 * 
	 * @return the ID of the MDT's ambulance
	 */
	public int getAmbulanceID() {
		return this.amb.getID();
	}

	/**
	 * @return true if the ambulance is logged, false otherwise
	 */
	public boolean isLogged() {
		return this.amb.isLogged();
	}

	/**
	 * 
	 * @return the list of all log entries for this MDT.
	 */
	synchronized public List<LogEntry> getLog() {
		return this.log;
	}

	/**
	 * Add a log entry.
	 * 
	 * @param entry
	 *            the log entry.
	 */
	synchronized private void addLogEntry(LogEntry entry) {
		this.log.add(entry);
	}

	/**
	 * 
	 * @return the Radio used by the MDT.
	 */
	public Radio getRadio() {
		return this.radio;
	}

	/**
	 * Set the MDTScreen for this MDT and set it as displayed.
	 * 
	 * @param gui
	 *            the MDTScreen
	 */
	public void setGUI(MDTScreen gui) {
		this.gui = gui;
	}

	/**
	 * 
	 * @return true if the MDTScreen is being displayed, false otherwise.
	 */
	public boolean isGUIDisplayed() {
		return this.gui != null;
	}

	/**
	 * Set the MDTScreen as removed from display
	 * 
	 */
	public void setGUIUndisplayed() {
		this.gui = null;
	}

	/**
	 * PRE : isGUIDisplayed() == true Set the focus on the MDTScreen.<br />
	 */
	public void focusOnGUI() {
		assert isGUIDisplayed();
		this.gui.requestFocus();
	}

	/**
	 * Returns the next position of the ambulance if it wants to go to dest.
	 * 
	 * @param dest
	 *            where the ambulance wants to go
	 * @return the next position on its shortest path to dest.
	 */
	public MapPosition getNextMapPosition(MapPosition dest) {
		List<MapPosition> path = this.gps.computePath(dest);
		if (path != null) {
			if (path.size() == 1) {
				return null;
			}
			return path.get(1);
		} else
			return null;
	}
}
